#!/usr/bin/env python2.4
# 
# Copyright (c) 2010 Ping Yeh, http://pingyeh.net/.
# 
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

def urange(x1, x2=None, dx=None):
  '''Iterate from x1 to x2 with a reasonable default x1 and dx.

  This function assumes the following methods are provided for the input:
  - The objects x1 and x2 must have __sub__() method.
  - The objects x1 must have __cmp__() or both __gt__() and __lt__() methods.
  - The object x1 must have __add__() with dx as the argument, and returns
    an object with the same class of x1.
  - The class of x1 must have a constructor that takes integer 0 as the only
    argument if x2 is missing.
  - If dx is given, it must be of the same class as x1 - x2.
  - The class of dx must have a constructor that takes an integer 0 or 1 as
    the only argument.
  - The class of dx must support __gt__() and __lt__().

  Examples:
    for i in urange(3.5, 9.3) yields [3.5, 4.5, 5.5, 6.5, 7.5, 8.5]
    for d in urange(date(2010,6,1), date(2010,6,3)) yeilds
          [date(2010,6,1),date(2010,6,2)]
    from datetime import datetime, timedelta
    for t in urange(datetime(2010,6,1,10,30,0), datetime(2010,6,1,11,0,0),
                    timedelta(minutes=5))
          yeilds 6 datetime objects 5 minutes apart as expected.

  See the unit tests in urange_test.py for more examples.

  Args:
    x1: start value.
    x2: end value, return value can't be equal to x2.
    dx: the delta of each step.
  Yields:
    x1, x1 + dx, ... until it is >= x2 (if dx > 0) or <= x2 (if dx < 0).
  Raises:
    TypeError if the criteria on methods of x1, x2 or dx are not met.
  '''
  # Interface check on data class (x1 and x2)
  if not hasattr(x1, '__sub__'):
    raise TypeError('class %s does not have __sub__().' % x1.__class__)
  if not hasattr(x1, '__add__') and not hasattr(x1, '__iadd__'):
    raise TypeError('class %s does not have __add__() or __iadd__().' % x1.__class__)
  if not x2:
    x2 = x1
    try:
      x1 = x2.__class__(0)
    except:
      raise TypeError('class %s does not support __init__(0), '
                      'default x1 is not possible.' % x2.__class__)
  # Interface check on delta class (dx)
  delta = x2 - x1
  if (not hasattr(delta, '__lt__') or not hasattr(delta, '__gt__')) \
     and not hasattr(delta, '__cmp__'):
    raise TypeError('class %s must have __cmp__() or both __lt__() and __gt__().' %
                    delta.__class__)
  if not dx:  # default: increment by 1
    dx = delta.__class__(1)
  # check the signs of delta and dx
  zero = delta.__class__(0)
  if (delta > zero and dx < zero or
      delta < zero and dx > zero):
    raise StopIteration
  # define the termination predicate
  if delta > zero:
    if not hasattr(x1, '__lt__') and not hasattr(x1, '__cmp__'):
      raise TypeError('class %s does not have __lt__() and __cmp__().' % x1.__class__)
    should_continue = lambda x, y: x < y
  else:
    if not hasattr(x1, '__gt__') and not hasattr(x1, '__cmp__'):
      raise TypeError('class %s does not have __gt__() and __cmp__().' % x1.__class__)
    should_continue = lambda x, y: x > y
  # start iterating
  x = x1
  while should_continue(x, x2):
    yield x
    x += dx
  raise StopIteration


